/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.executor;

/**
 * @author Clinton Begin
 */

/**
 * MyBatis 还有一个很有意思的点在于异常日志的输出。
 * 不知道大家有没有发现，使用 MyBatis 时定位问题非常容易，
 * 我们只需要查看一下控制台的异常日志就能一目了然地知道问题出现在了哪里
 */
public class ErrorContext {

  /**
   * MyBatis 异常涵盖的信息总结为一点就是：
   *    异常是由谁在做什么的时候在哪个资源文件中发生的，
   *    执行的 SQL 是哪个，以及 java 详细的异常信息。
   */
  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  /**
   * ThreadLocal 管理 ErrorContext
   * ThreadLocal 是本地线程存储，
   * 它的作用是为变量在每个线程中创建一个副本，
   * 每个线程内部都可以使用该副本，线程之间互不影响
   *
   * ErrorContext 可以看做是线程内部的单例模式
   *
   * 使用 ThreadLocal 来管理 ErrorContext：
   * 保证了在多线程环境中，每个线程内部可以共用一份 ErrorContext，
   * 但多个线程持有的 ErrorContext 互不影响，保证了异常日志的正确输出。
   *
   */
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();


  private ErrorContext stored;
  /**
   * 存储异常存在于哪个资源文件中。
   */
  private String resource;
  /**
   * 存储异常是做什么操作时发生的。
   */
  private String activity;
  /**
   * 存储哪个对象操作时发生异常。
   */
  private String object;
  /**
   * 存储异常的概览信息。
   */
  private String message;
  /**
   * 存储发生日常的 SQL 语句。
   */
  private String sql;
  /**
   * 存储详细的 Java 异常日志。
   */
  private Throwable cause;

  private ErrorContext() {
  }

  /**
   * 提供获取线程内实例的静态公有方法，
   * @return
   */
  public static ErrorContext instance() {
    // 先从本地线程获取
    ErrorContext context = LOCAL.get();
    //如果没有 这个实例，创建，再存入 本地线程 LOCAL
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }

  public ErrorContext store() {
    stored = this;
    LOCAL.set(new ErrorContext());
    return LOCAL.get();
  }

  public ErrorContext recall() {
    if (stored != null) {
      LOCAL.set(stored);
      stored = null;
    }
    return LOCAL.get();
  }

  public ErrorContext resource(String resource) {
    this.resource = resource;
    return this;
  }

  public ErrorContext activity(String activity) {
    this.activity = activity;
    return this;
  }

  public ErrorContext object(String object) {
    this.object = object;
    return this;
  }

  public ErrorContext message(String message) {
    this.message = message;
    return this;
  }

  public ErrorContext sql(String sql) {
    this.sql = sql;
    return this;
  }

  public ErrorContext cause(Throwable cause) {
    this.cause = cause;
    return this;
  }

  public ErrorContext reset() {
    resource = null;
    activity = null;
    object = null;
    message = null;
    sql = null;
    cause = null;
    LOCAL.remove();
    return this;
  }

  @Override
  public String toString() {
    StringBuilder description = new StringBuilder();

    // message
    if (this.message != null) {
      description.append(LINE_SEPARATOR);
      description.append("### ");
      description.append(this.message);
    }

    // resource
    if (resource != null) {
      description.append(LINE_SEPARATOR);
      description.append("### The error may exist in ");
      description.append(resource);
    }

    // object
    if (object != null) {
      description.append(LINE_SEPARATOR);
      description.append("### The error may involve ");
      description.append(object);
    }

    // activity
    if (activity != null) {
      description.append(LINE_SEPARATOR);
      description.append("### The error occurred while ");
      description.append(activity);
    }

    // activity
    if (sql != null) {
      description.append(LINE_SEPARATOR);
      description.append("### SQL: ");
      description.append(sql.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').trim());
    }

    // cause
    if (cause != null) {
      description.append(LINE_SEPARATOR);
      description.append("### Cause: ");
      description.append(cause.toString());
    }

    return description.toString();
  }

}
